/**
 * Copyright (C) 2007-2009 Felipe Contreras
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "pn_printf.h"

#ifdef PN_CUSTOM_PRINTF

/*
 * This implementation was copied from the klibc project.
 */

#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdarg.h>

enum flags {
    FL_ZERO = 0x01,
    FL_MINUS = 0x02,
    FL_PLUS = 0x04,
    FL_TICK = 0x08,
    FL_SPACE = 0x10,
    FL_HASH = 0x20,
    FL_SIGNED = 0x40,
    FL_UPPER = 0x80,
};

enum ranks {
    rank_char = -2,
    rank_short = -1,
    rank_int = 0,
    rank_long = 1,
    rank_longlong = 2,
};

#define MIN_RANK rank_char
#define MAX_RANK rank_longlong

#define INTMAX_RANK rank_longlong
#define SIZE_T_RANK rank_long
#define PTRDIFF_T_RANK rank_long

#define EMIT(x) ({ if (o < n) { *q++ = (x); } o++; })

static size_t
format_int(char *q,
           size_t n,
           uintmax_t val,
           enum flags flags,
           int base,
           int width,
           int prec)
{
    char *qq;
    size_t o = 0, oo;
    static const char lcdigits[] = "0123456789abcdef";
    static const char ucdigits[] = "0123456789ABCDEF";
    const char *digits;
    uintmax_t tmpval;
    int minus = 0;
    int ndigits = 0, nchars;
    int tickskip, b4tick;

    /* Select type of digits */
    digits = (flags & FL_UPPER) ? ucdigits : lcdigits;

    /* If signed, separate out the minus */
    if (flags & FL_SIGNED && (intmax_t) val < 0) {
        minus = 1;
        val = (uintmax_t) (-(intmax_t) val);
    }

    /* Count the number of digits needed.  This returns zero for 0. */
    tmpval = val;
    while (tmpval) {
        tmpval /= base;
        ndigits++;
    }

    /* Adjust ndigits for size of output */

    if (flags & FL_HASH && base == 8) {
        if (prec < ndigits + 1)
            prec = ndigits + 1;
    }

    if (ndigits < prec)
        ndigits = prec; /* Mandatory number padding */
    else if (val == 0)
        ndigits = 1; /* Zero still requires space */

    /* For ', figure out what the skip should be */
    if (flags & FL_TICK)
        tickskip = (base == 16) ? 4 : 3;
    else
        tickskip = ndigits; /* No tick marks */

    /* Tick marks aren't digits, but generated by the number converter */
    ndigits += (ndigits - 1) / tickskip;

    /* Now compute the number of nondigits */
    nchars = ndigits;

    if (minus || (flags & (FL_PLUS | FL_SPACE)))
        nchars++; /* Need space for sign */
    if ((flags & FL_HASH) && base == 16)
        nchars += 2; /* Add 0x for hex */

    /* Emit early space padding */
    if (!(flags & (FL_MINUS | FL_ZERO)) && width > nchars) {
        while (width > nchars) {
            EMIT(' ');
            width--;
        }
    }

    /* Emit nondigits */
    if (minus)
        EMIT('-');
    else if (flags & FL_PLUS)
        EMIT('+');
    else if (flags & FL_SPACE)
        EMIT(' ');

    if ((flags & FL_HASH) && base == 16) {
        EMIT('0');
        EMIT((flags & FL_UPPER) ? 'X' : 'x');
    }

    /* Emit zero padding */
    if ((flags & (FL_MINUS | FL_ZERO)) == FL_ZERO && width > ndigits) {
        while (width > nchars) {
            EMIT('0');
            width--;
        }
    }

    /* Generate the number.  This is done from right to left. */
    q += ndigits; /* Advance the pointer to end of number */
    o += ndigits;
    qq = q;
    oo = o; /* Temporary values */

    b4tick = tickskip;
    while (ndigits > 0) {
        if (!b4tick--) {
            qq--;
            oo--;
            ndigits--;
            if (oo < n)
                *qq = '_';
            b4tick = tickskip - 1;
        }
        qq--;
        oo--;
        ndigits--;
        if (oo < n)
            *qq = digits[val % base];
        val /= base;
    }

    /* Emit late space padding */
    while ((flags & FL_MINUS) && width > nchars) {
        EMIT(' ');
        width--;
    }

    return o;
}

static int
pn_vsnprintf(char *buffer,
             size_t n,
             const char *format,
             va_list ap)
{
    const char *p = format;
    char ch;
    char *q = buffer;
    size_t o = 0; /* Number of characters output */
    uintmax_t val = 0;
    int rank = rank_int; /* Default rank */
    int width = 0;
    int prec = -1;
    int base;
    size_t sz;
    enum flags flags = 0;
    enum {
        st_normal, /* Ground state */
        st_flags, /* Special flags */
        st_width, /* Field width */
        st_prec, /* Field precision */
        st_modifiers /* Length or conversion modifiers */
    } state = st_normal;
    const char *sarg; /* %s string argument */
    char carg; /* %c char argument */
    int slen; /* String length */

    while ((ch = *p++)) {
        switch (state) {
            case st_normal:
                if (ch == '%') {
                    state = st_flags;
                    flags = 0;
                    rank = rank_int;
                    width = 0;
                    prec = -1;
                }
                else
                    EMIT(ch);
                break;

            case st_flags:
                switch (ch) {
                    case '-':
                        flags |= FL_MINUS;
                        break;
                    case '+':
                        flags |= FL_PLUS;
                        break;
                    case '\'':
                        flags |= FL_TICK;
                        break;
                    case ' ':
                        flags |= FL_SPACE;
                        break;
                    case '#':
                        flags |= FL_HASH;
                        break;
                    case '0':
                        flags |= FL_ZERO;
                        break;
                    default:
                        state = st_width;
                        p--;	/* Process this character again */
                        break;
                }
                break;

            case st_width:
                if (ch >= '0' && ch <= '9') {
                    width = width * 10 + (ch - '0');
                }
                else if (ch == '*') {
                    width = va_arg(ap, int);
                    if (width < 0) {
                        width = -width;
                        flags |= FL_MINUS;
                    }
                }
                else if (ch == '.') {
                    prec = 0;	/* Precision given */
                    state = st_prec;
                }
                else {
                    state = st_modifiers;
                    p--;	/* Process this character again */
                }
                break;

            case st_prec:
                if (ch >= '0' && ch <= '9') {
                    prec = prec * 10 + (ch - '0');
                }
                else if (ch == '*') {
                    prec = va_arg(ap, int);
                    if (prec < 0)
                        prec = -1;
                }
                else {
                    state = st_modifiers;
                    p--;	/* Process this character again */
                }
                break;

            case st_modifiers:
                switch (ch) {
                    /* Length modifiers - nonterminal sequences */
                    case 'h':
                        rank--;	/* Shorter rank */
                        break;
                    case 'l':
                        rank++;	/* Longer rank */
                        break;
                    case 'j':
                        rank = INTMAX_RANK;
                        break;
                    case 'z':
                        rank = SIZE_T_RANK;
                        break;
                    case 't':
                        rank = PTRDIFF_T_RANK;
                        break;
                    case 'L':
                    case 'q':
                        rank += 2;
                        break;
                    default:
                        /* Output modifiers - terminal sequences */

                        /* Next state will be normal */
                        state = st_normal;

                        /* Canonicalize rank */
                        if (rank < MIN_RANK)
                            rank = MIN_RANK;
                        else if (rank > MAX_RANK)
                            rank = MAX_RANK;

                        switch (ch) {
                            case 'P': /* Upper case pointer */
                                flags |= FL_UPPER;
                                /* fall through */
                            case 'p': /* Pointer */
                                val = (uintmax_t) (uintptr_t) va_arg(ap, void *);
                                if (!val) {
                                    sarg = "(nil)";
                                    slen = strlen(sarg);
                                    goto is_string;
                                }

                                base = 16;
                                /* I like this better, but it's not comformant */
                                /* prec = (CHAR_BIT * sizeof(void *) + 3) / 4; */
                                flags |= FL_HASH;
                                goto is_integer;

                            case 'd': /* Signed decimal output */
                            case 'i':
                                base = 10;
                                flags |= FL_SIGNED;
                                switch (rank) {
                                    case rank_char:
                                        /* Yes, all these casts are needed */
                                        val = (uintmax_t) (intmax_t) (signed char) va_arg(ap, signed int);
                                        break;
                                    case rank_short:
                                        val = (uintmax_t) (intmax_t) (signed short) va_arg(ap, signed int);
                                        break;
                                    case rank_int:
                                        val = (uintmax_t) (intmax_t) va_arg(ap, signed int);
                                        break;
                                    case rank_long:
                                        val = (uintmax_t) (intmax_t) va_arg(ap, signed long);
                                        break;
                                    case rank_longlong:
                                        val = (uintmax_t) (intmax_t) va_arg(ap, signed long long);
                                        break;
                                }
                                goto is_integer;
                            case 'o': /* Octal */
                                base = 8;
                                goto is_unsigned;
                            case 'u': /* Unsigned decimal */
                                base = 10;
                                goto is_unsigned;
                            case 'X': /* Upper case hexadecimal */
                                flags |= FL_UPPER;
                                /* fall through */
                            case 'x': /* Hexadecimal */
                                base = 16;
                                goto is_unsigned;

is_unsigned:
                                switch (rank) {
                                    case rank_char:
                                        val = (uintmax_t) (unsigned char) va_arg(ap, unsigned int);
                                        break;
                                    case rank_short:
                                        val = (uintmax_t) (unsigned short) va_arg(ap, unsigned int);
                                        break;
                                    case rank_int:
                                        val = (uintmax_t) va_arg(ap, unsigned int);
                                        break;
                                    case rank_long:
                                        val = (uintmax_t) va_arg(ap, unsigned long);
                                        break;
                                    case rank_longlong:
                                        val = (uintmax_t) va_arg(ap, unsigned long long);
                                        break;
                                }
                                /* fall through */

is_integer:
                                sz = format_int(q, (o < n) ? n - o : 0,
                                                val, flags, base, width, prec);
                                q += sz;
                                o += sz;
                                break;

                            case 'c':	/* Character */
                                carg = (char) va_arg(ap, int);
                                sarg = &carg;
                                slen = 1;
                                goto is_string;
                            case 's':	/* String */
                                sarg = va_arg(ap, const char *);
                                sarg = sarg ? sarg : "(null)";
                                slen = strlen(sarg);
                                goto is_string;

is_string:
                                {
                                    char sch;
                                    int i;

                                    if (prec != -1 && slen > prec)
                                        slen = prec;

                                    if (width > slen && !(flags & FL_MINUS)) {
                                        char pad = (flags & FL_ZERO) ? '0' : ' ';
                                        while (width > slen) {
                                            EMIT(pad);
                                            width--;
                                        }
                                    }
                                    for (i = slen; i; i--) {
                                        sch = *sarg++;
                                        EMIT(sch);
                                    }
                                    if (width > slen && (flags & FL_MINUS)) {
                                        while (width > slen) {
                                            EMIT(' ');
                                            width--;
                                        }
                                    }
                                }
                                break;

                            default:
                                EMIT(ch);
                                break;
                        }
                }
        }
    }

    /* Null-terminate the string */
    if (o < n)
        *q = '\0'; /* No overflow */
    else if (n > 0)
        buffer[n - 1] = '\0'; /* Overflow - terminate at end of buffer */

    return o;
}

static int
pn_vasprintf(char **bufp,
             const char *format,
             va_list ap)
{
    va_list ap1;
    int bytes;
    char *p;

    va_copy(ap1, ap);

    bytes = pn_vsnprintf(NULL, 0, format, ap1) + 1;
    va_end(ap1);

    *bufp = p = malloc(bytes);
    if (!p)
        return -1;

    return pn_vsnprintf(p, bytes, format, ap);
}

char *
pn_strdup_vprintf(const char *format,
                  va_list args)
{
    char *buffer;
    pn_vasprintf(&buffer, format, args);
    return buffer;
}

char *
pn_strdup_printf(const char *format,
                 ...)
{
    char *buffer;
    va_list args;

    va_start(args, format);
    buffer = pn_strdup_vprintf(format, args);
    va_end(args);

    return buffer;
}

#endif /* PN_CUSTOM_PRINTF */
